<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>18.2.3-将虚拟 DOM 渲染为 HTML 字符串</title>
</head>
<body>
    
</body>
    <script>
        /**
         * 2. 需要更严谨地处理 HTML 属性 处理属性需要考虑多个方面，
         * 
         * 首先是对 boolean attribute 的处理
         * - 例如 <input/> 标签的 checked 属性和 disabled 属性
         * 
         * 另外一点需要考虑的是安全问题
         * - 属性名称必须由一个或多个非以下字符组成
         * -- 控制字符集（control character）的码点范围是：[0x01, 0x1f] 和 [0x7f,0x9f]。
         * -- U+0020 (SPACE)、U+0022 (")、U+0027 (')、U+003E (>)、U+002F (/) 以及 U+003D (=)。
         * -- noncharacters，这里的 noncharacters 代表 Unicode 永久保留的码点，
         *     这些码点在 Unicode 内部使用，它的取值范围是：[0xFDD0,0xFDEF]，
         *     还包括：0xFFFE、0xFFFF、0x1FFFE、0x1FFFF、0x2FFFE、0x2FFFF、0x3FFFE、0x3FFFF、、、、
         * Vue.js 的模板编译器在编译过程中已经对 noncharacters 以及控制字符集进行了处理，
         *  所以我们只需要小范围处理即可，
         *  任何不满足上述条件的属性名称都是不安全且不合法的
         * 
         * 另外，在虚拟节点中的 props 对象中，通常会包含仅用于组件运行时逻辑的相关属性
         *  例如，key 属性仅用于虚拟 DOM 的 Diff 算法，
         *  ref 属性仅用于实现 template ref 的功能等
         *  服务端渲染也无须考虑事件绑定。
         *  在进行服务端渲染时，应该忽略这些属性
         * 
         * 对于使用不同数据结构表示的 class 或style，
         *  我们只需要将不同类型的数据结构序列化成字符串表示即可
         * 
         * 调用了 escapeHtml 对其进行转义处理，这对于防御XSS 攻击至关重要
         * - HTML 转义指的是将特殊字符转换为对应的 HTML 实体
         * 
         * 转换规则
         * - 如果该字符串作为普通内容被拼接，则应该对以下字符进行转义
         * -- 将字符 & 转义为实体 &amp;。
         * -- 将字符 < 转义为实体 &lt;
         * -- 将字符 > 转义为实体 &gt;
         * - 如果该字符串作为属性值被拼接，那么除了上述三个字符应该被转义之外，还应该转义下面两个字符
         * -- 将字符 " 转义为实体 &quot;
         * -- 将字符 ' 转义为实体 &#39;
        */


        
        
        // 给出如下虚拟节点对象
        const ElementVNode = {
            type: 'div',
            props: {
                id: 'foo',
                checked: true,
                disabled: false,
                'data-foo': 'bar',
                class: ['foo', 'bar'],
                is: '2 < 0'
            },
            children: [
                { type: 'p', children: 'hello' },
                { type: 'input', children: 'hello' },
            ]
        }

        // 应该忽略的属性
        const shouldIgnoreProp = ['key', 'ref'] // 新增

        function renderAttrs(props) { // 新增
            let ret = ''
            for (const key in props) {
                if (
                    // 检测属性名称，如果是事件或应该被忽略的属性，则忽略它
                    shouldIgnoreProp.includes(key) ||
                    /^on[^a-z]/.test(key)
                ) {
                    continue
                }
                const value = props[key]
                // 调用 renderDynamicAttr 完成属性的渲染
                ret += renderDynamicAttr(key, value)
            }
            return ret
        }

        // 用来判断属性是否是 boolean attribute
        const isBooleanAttr = (key) => // 新增
            (`itemscope,allowfullscreen,formnovalidate,ismap,nomodule,novalidate,readonly` +
            `,async,autofocus,autoplay,controls,default,defer,disabled,hidden,` +
            `loop,open,required,reversed,scoped,seamless,` +
            `checked,muted,multiple,selected`).split(',').includes(key)

        // 用来判断属性名称是否合法且安全
        const isSSRSafeAttrName = (key) => !/[>/="'\u0009\u000a\u000c\u0020]/.test(key) // 新增

        function renderDynamicAttr(key, value) {
            if (isBooleanAttr(key)) {
                // 对于 boolean attribute，如果值为 false，则什么都不需要渲染，否则只需要渲染 key 即可
                return value === false ? `` : ` ${key}`
            } else if (isSSRSafeAttrName(key)) {
                // 对于其他安全的属性，执行完整的渲染，
                // 注意：对于属性值，我们需要对它执行 HTML 转义操作
                return value === '' ? ` ${key}` : ` ${key}="${escapeHtml(value)}"`
            } else {
                // 跳过不安全的属性，并打印警告信息
                console.warn(
                    `[@vue/server-renderer] Skipped rendering unsafe attribute name: ${key}`
                )
                return ``
            }
        }

        const escapeRE = /["'&<>]/ // 新增
        function escapeHtml(string) { // 新增
            const str = '' + string
            const match = escapeRE.exec(str)

            if (!match) {
                return str
            }

            let html = ''
            let escaped
            let index
            let lastIndex = 0
            for (index = match.index; index < str.length; index++) {
                switch (str.charCodeAt(index)) {
                    case 34: // "
                        escaped = '&quot;'
                        break
                    case 38: // &
                        escaped = '&amp;'
                        break
                    case 39: // '
                        escaped = '&#39;'
                        break
                    case 60: // <
                        escaped = '&lt;'
                        break
                    case 62: // >
                        escaped = '&gt;'
                        break
                    default:
                        continue
                }

                if (lastIndex !== index) {
                    html += str.substring(lastIndex, index)
                }

                lastIndex = index + 1
                html += escaped
            }

            return lastIndex !== index ? html + str.substring(lastIndex, index) : html
        }




        
        const VOID_TAGS = 'area,base,br,col,embed,hr,img,input,link,meta,param,source,track,wbr'.split(',')
        // 在不考虑任何边界条件的情况下，实现 renderElementVNode
        function renderElementVNode(vnode){
            // 取出标签名称 tag 和标签属性 props，以及标签的子节点
            const { type: tag, props, children } = vnode
            // 判断是否是 void element
            const isVoidElement = VOID_TAGS.includes(tag)
            // 开始标签的头部
            let ret = `<${tag}`
            // 处理标签属性
            if (props) {
                // 调用 renderAttrs 函数进行严谨处理
                ret += renderAttrs(props) // 新增
            }

            // 如果是 void element，则自闭合
            ret += isVoidElement ? `/>` : `>` 
            // 如果是 void element，则直接返回结果，无须处理 children，因为 void element 没有 children
            if (isVoidElement) return ret 

            // 处理子节点
            // 如果子节点的类型是字符串，则是文本内容，直接拼接
            if (typeof children === 'string') {
                ret += children
            } else if (Array.isArray(children)) {
                // 如果子节点的类型是数组，则递归地调用 renderElementVNode 完成渲染
                children.forEach(child => {
                    ret += renderElementVNode(child)
                })
            }

            // 结束标签
            ret += `</${tag}>`

            // 返回拼接好的 HTML 字符串
            return ret
        }

        
        
        
        // 测试
        console.log(renderElementVNode(ElementVNode))
    </script>
</html>